Development
Run these checks from the repository root. The builder loads configuration, hooks, package recipes, and assets from the checkout, so use a complete source tree.
Set up a development environment
python3 -m pip install uv==0.11.23
uv sync --locked --extra dev --extra docs
. .venv/bin/activate
uv.lock pins the dependency resolution across supported Python versions.
Use uv sync --locked so the environment matches the committed lockfile and
does not rewrite it during setup.
On Debian or Ubuntu, install the system tools needed for mock integration tests
and translation validation with sudo apt install gettext mtools dosfstools xorriso squashfs-tools.
Validation
python cli.py --check
python -m pytest -q
git ls-files -z '*.py' | xargs -0 -r flake8
pylint --errors-only void_builder cli.py
python tools/check_bandit_regressions.py
python tools/check_pylint_regressions.py
bandit -r configs/custom_files -ll
python tools/check_release_licenses.py --inventory-only
git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n
yamllint -c .yamllint .forgejo/workflows
yamllint -c .yamllint-configs configs
python -m build --no-isolation --wheel --outdir /tmp/pepvoid-dist
find configs/custom_files -type f -name '*.po' -print0 \
| xargs -0 -r -n1 msgfmt --check --check-format -o /dev/null
find configs/custom_files -type f -name '*.pot' -print0 \
| xargs -0 -r -n1 msgfmt --check-format -o /dev/null
sphinx-build -b html -W --keep-going docs /tmp/pepvoid-docs
The Codeberg/Forgejo Actions workflows run configuration checks, Python and
workflow YAML lint, wheel builds, translation validation, pytest, and the
documentation build on Python 3.9–3.14. Jobs use the matching Python container
and install a checksum-verified Node.js 24.22.2 runtime for JavaScript Actions.
To run them on Codeberg, enable the repository’s Actions unit and provide a
Forgejo Runner with the docker label;
the jobs request that label and use Python container images. Codeberg’s hosted
Actions service is currently limited, so a self-hosted runner may be required
(Codeberg Actions setup,
Forgejo runner labels).
The autonomous mock artifact matrix runs on Python 3.12 and covers ISO, raw
disk, virtual machine, container, and tarball outputs. Pytest
checks project SPDX headers, license texts, and attributions for selected
bundled assets; CI separately parses tracked shell scripts with Bash. These
checks do not prove that every third-party asset has been identified. The
26 IceWM XPM images have a hash-pinned maintainer confirmation recorded in
docs/licensing/maintainer-confirmations.json; maintainers must add unresolved
records whenever another asset’s source, license, or permission is unclear. Consult
the licensing and provenance guide
and re-audit the manifest when bundled assets change. The Release Licensing Gate workflow verifies hash-pinned asset and repository-history records, then
rejects version tags while the unresolved manifest contains files or trees; run
python3 tools/check_release_licenses.py locally to check file hashes,
vendored tree contents (including symlink targets), and repository-history
integrity, and to see any blockers. --inventory-only validates all recorded
evidence and rejects unreviewed payload assets while allowing documented
release blockers, so regular CI remains useful during rights review. The release
tag workflow runs the strict gate. It currently reports 25 generated wallpaper
assets and 25 original-photo blobs retained in Git history; do not publish a
release until rights evidence is recorded or those assets and historical blobs
are removed. Generated icon-theme.cache files are
excluded from the Tela Circle tree digest. The CI
workflow builds the Sphinx documentation on pull requests with warnings treated
as errors; the Forgejo Pages workflow also builds it before deployment. Flake8
checks style and Pyflakes diagnostics across all tracked Python source files.
Pylint blocks Python errors across the matrix and compares all finding counts
with the reviewed baseline in tools/pylint-baseline.json on Python 3.12.
This keeps the known cleanup work visible while preventing new findings. The
baseline is tied to Pylint 3.3.4 and targets Python 3.12; review the full report
before fixing or updating it. Python 3.14 uses Pylint 4.1.3 for interpreter
compatibility; the baseline check remains on Python 3.12. The first reviewed
baseline recorded 628 diagnostics. The current baseline has 18 findings: seven
R0903 small-interface/data-record warnings, three R0902 warnings for
lifecycle managers, and four methods each reported for R0913 and R0917
because their constructors or command API carry 6–7 explicit inputs. There is
no context-manager warning in the current baseline. Each remaining
path/message pair has a rationale in tools/pylint-baseline.json; the baseline
checker rejects missing or stale rationales as well as diagnostic count
increases. Earlier cleanups cleared lazy logging interpolation, unspecified
text encoding warnings, protected-member accesses, and duplicate code across
bootloader configuration, preset aliases, Xfce package policy, and CLI code.
The R0903 cases are intentional single-operation facades, an abstract
lifecycle interface, the engine registry, and a verification record; adding
dummy public methods would make those APIs less clear. The R0902 cases hold
state with one build/toolchain lifecycle. The parameter-count findings belong
to explicit dependency/context constructors and a subprocess-style runner API;
they remain candidates for a request object if their call-site complexity
grows. Recent cleanup removed local-import notices, simplified profile
metadata dispatch, removed unnecessary else branches, and
moved state initialization into constructors. System-configuration assembly is split into
focused phases for profiles, platforms, package rules, user overrides, and
repository selection. Configuration records and architecture/libc package
filtering now live in the focused config_models.py and
config_package_policy.py modules. Architecture-specific repository selection
is isolated in config_repository.py; configuration assembly and artifact
naming inputs are grouped into typed request records. The command-line argument definitions and
profile/artifact naming helpers now live in cli_parser.py and cli_helpers.py,
leaving cli.py focused on dispatching requested operations. CLI operations
are grouped in cli_actions.py, option normalization in cli_build_options.py,
and build setup/reporting in cli_workflow.py; the parser registers distinct
argument groups through focused helpers. Target-system configuration actions
are grouped in customizer_actions.py and
customizer_desktop_actions.py, while customizer.py assembles and applies
them. Artifact manifests and root filesystem exports live in iso_artifacts.py,
the build driver in iso_builder.py, and architecture engine registration in
iso_engine_base.py. Package repository selection and XBPS install retries
now live in package_installer.py, leaving ChrootManager as the stable
filesystem and chroot facade. Disk-image
creation now separates boot
staging, image filesystems, partition layout, and output conversion/compression. Disk-image
verification now separates format detection, signature checks, compression
checks, and optional qemu-img inspection; ISO verification separates header
checks, directory listing, and member validation; tarball verification
separates directory, package database, ELF architecture, and shadow-mode
checks. Boot asset staging now lives in boot_asset_stager.py, separate from
the architecture engine. The orchestrator now passes its retry setting through
to the toolchain. The tarball resolver separates local references, HTTPS
downloads, and automatic cache lookup. Artifact checksums, manifest assembly,
and signing failure handling have separate helpers. Package installation
separates stage preparation, local repository indexing, and filtering of custom
packages without matching local archives. PRoot setup now separates
the native, cached, and pinned-download paths; the command runner separates
streamed and captured subprocess execution.
Three broad exception catches remain at intentional error boundaries: the CLI
converts unexpected top-level failures into a concise user-facing error, the
build pipeline wraps unexpected failures while guaranteeing cleanup, and the
customizer reports failures from extensible action implementations before
propagating them. Download, extraction, signing, and toolchain paths catch
their expected error types explicitly. The other findings remain a cleanup
backlog, not a declaration that each finding is resolved. The baseline has
been lowered to the current scan so future increases fail CI. Bandit checks
all severity levels against exact reviewed source contexts in
tools/bandit-baseline.json. It compares the path, rule, severity, confidence,
message, and flagged source line (ignoring line-number changes), so a new call
fails CI even when it uses a rule already present in that file. The baseline also requires a written
rationale for every rule represented in its findings. The distributed applications under
configs/custom_files are also scanned for medium and high severity Bandit
findings. Their low severity subprocess notices are reviewed separately
because package management and desktop integration require invoking host tools.
The Bandit baseline is pinned to version 1.9.4 and contains 94 low severity
notices. They concern subprocess calls using
argument lists, the corresponding subprocess imports, and executable lookups
through PATH. Rule-specific rationales are recorded beside the findings.
The builder needs subprocesses to invoke host tools; the reviewed source
contexts remain candidates for further cleanup and the baseline should not be
regenerated just to silence them. Run bandit -r void_builder cli.py -l to
review the full report before changing the baseline.
Starting the Codeberg Actions runner
The repository runner must be online on the host while Codeberg Actions jobs
are waiting. Install forgejo-runner on that host and register a repository
runner in Settings → Actions → Runners. Keep the UUID from the registration
details and save the runner token in a separate file; never commit or paste the
token into chat.
Create a private config directory and token file, then create
~/.config/pepvoid-builder/codeberg-runner.json with this structure:
{
"uuid": "RUNNER-UUID-FROM-CODEBERG",
"token_file": "/home/your-user/.config/pepvoid-builder/runner-token"
}
Restrict access to both files (chmod 700 ~/.config/pepvoid-builder and
chmod 600 on the JSON config and token file). Start the runner from the
repository root and leave the terminal open:
./tools/start-codeberg-runner.sh
The script reads the UUID and token path locally, then starts the registered
runner with the docker label used by the repository workflows. Override the
config or runner executable paths with CODEBERG_RUNNER_CONFIG and
FORGEJO_RUNNER_BIN if needed. The runner token itself is never passed as a
command-line argument or printed by the script.